 <?php
/*

Copyright (c) 2010 four.zero.one.unauthorized

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Author: four.zero.one.unauthorized@gmail.com
Initial Build Date: 12/14/2010
Rev Date: 12/14/2010 22:00
Summary: ROUGH DRAFT - Using SPL extension to demonstrate how to convert a MySQLi_Result or MySQLi_STMT object into an array

Extensions:
1) SPL
2) MySQLi

Dependencies: 
1) PHP: 5.3

References:
1) SPL PHP Manual: http://php.net/manual/en/book.spl.php
2) MySQLi PHP manual: http://php.net/manual/en/book.mysqli.php
3) About this code, examples: http://code.google.com/p/corporate-overload/wiki/MySQLi_Recordset

*/
   

class MySQLi_Recordset implements Iterator, ArrayAccess, Countable {

    private $MySQLi_set; // MySQLi_STMT or MySQLi_Result object
    private $MySQLi_set_type; // cache of MySQLi_STMT or MySQLi_Result object type to avoid repeat instanceof calls
    private $field_metadata; // array of field info objects returned by query
    private $num_rows; // cache row count to avoid repeat mysqli_x->num_rows call
    private $bind_row; // binding array for MySQLi_STMT results
    private $pointer; // for tracking iterations over records in query
    
        
    function __construct($sql) {
        // Wraps object resulting from mysqli query or statement (MySQLi_STMT or MySQLi_Result object).
        // If only $sql argument is passed in, it will be treated as a regular query.
        // If additional arguments are passed in, it will be treated as a statement.
        // Handling of MySQLi_STMT or MySQLi_Result object may differ, so code paths may fork as needed, but are intended to end with the same output.
        // If wrapped correctly, $this->field_metadata will be an array of objects with
        // properties describing each field.  See: http://www.php.net/manual/en/mysqli-result.fetch-fields.php
        // Any MySQLi_STMT will be 'stored' to allow 'offset' and 'count' to work.
        $bind_params = func_get_args();
        array_shift($bind_params);
        try {
            if (count($bind_params) === 0)
                $this->MySQLi_set = DB_Main::query($sql);  // expecting MYSQLI_Result obj returned, replace code as necessary
            else
                $this->MySQLi_set = DB_Main::statement($sql, $bind_params);  // expecting MYSQLI_STMT obj returned, replace code as necessary
            }
        catch (Exception $e) {
            throw new RuntimeException(((count($bind_params)) ? 'Statement' : 'Query') . ' failed:  ' . $e->getMessage(), $e->getCode(), $e);
            }
        if (!($this->MySQLi_set instanceof MySQLi_STMT OR $this->MySQLi_set instanceof MySQLi_Result))
            throw new RuntimeException('No MySQLi_STMT or MySQLi_Result object to work with.');
        // cache name of object to avoid repeat instanceof calls
        $this->MySQLi_set_type = get_class($this->MySQLi_set);
        if ($this->MySQLi_set_type === 'MySQLi_Result')
            // Set up special handling for MySQLi_Result objects
            // set $this->field_metadata to array of field info objects
            $this->field_metadata = $this->MySQLi_set->fetch_fields();
        else {
            // Set up special handling for MYSQLI_STMT objects
            // set $this->field_metadata to array of field info objects used later for result bindings for MYSQLI_STMT object
            $result_metadata = $this->MySQLi_set->result_metadata();
            $this->field_metadata = $result_metadata->fetch_fields();    
            $result_metadata->free_result();
            unset($result_metadata);
            // store results to allow object to act as a countable, seekable array
            if (!$this->MySQLi_set->store_result())
                throw new RuntimeException('MySQLi_STMT Result failed to store.  Error: ' . $this->MySQLi_set->error);
            // because bind_result() cannot bind to an object or array, use the 'call_user_func_array()' technique instead
            $this->bind_row = array();
            $bind_row2 = array();
            foreach($this->field_metadata as $field)
                 $bind_row2[$field->name] =& $this->bind_row[$field->name];
            if (!call_user_func_array(array($this->MySQLi_set, 'bind_result'), $this->bind_row))
                throw new RuntimeException($this->MySQLi_set->error);
            if ($this->MySQLi_set->error)
                throw new RuntimeException($this->MySQLi_set->error);
            }
        // cache row count to avoid constant calls to MySQLi_x->num_rows
        $this->num_rows = $this->MySQLi_set->num_rows;
        // if rows were returned, set internal pointer to 0
        if ($this->num_rows > 0) $this->pointer = 0;
        }


    function __destruct() {
        // Handle freeing or closing queries where needed
        if ($this->MySQLi_set_type === 'MySQLi_Result')
            $this->MySQLi_set->free();
        else {
            $this->MySQLi_set->free_result();
            $this->MySQLi_set->close();
            }
        $this->MySQLi_set = null;
        }

        
    function __call($fnc, $args) {
        // Allow accessing the wrapped MySQLi_set object methods to allow greater flexibility
        return call_user_func_array(array($this->MySQLi_set, $fnc), $args);
        }
        
        
    function __get($prop) {
        // Allow accessing the wrapped MySQLi_set object properties to allow greater flexibility
        return $this->MySQLi_set->$prop;
        }
        
        
    function fetchRow($offset = null) {
        // private function internally using regular fetch() methods to progress through records
        // sets $this->current_row to an associative array of the next record
        $this->MySQLi_set->data_seek((($offset !== null) ? (int) $offset : $this->pointer));
        if ($this->MySQLi_set_type === 'MySQLi_Result')
            return $this->MySQLi_set->fetch_assoc();
        // else this is a MySQLi_STMT object
        $fetch_status = $this->MySQLi_set->fetch();
        if ($fetch_status === true) return $this->bind_row;
        else if ($fetch_status === null) return null; // no more rows
        // MySQLi_STMT row fetch failed
        throw new RuntimeException('Error fetching row: . ' . $this->MySQLi_set->error);
        }
    
    
    function fetchFields() {
        // if wrapped correctly, $this->field_metadata will be an array of objects with
        // properties describing each field.  See: http://www.php.net/manual/en/mysqli-result.fetch-fields.php
        return $this->field_metadata;
        }
        
    
    function getResultType() {
        // to see if wrapped mysqli object is either a statement or a query result object
        return $this->MySQLi_set_type;
        }
        
        
    function count() {
        // part of the Countable interface.  Used for counting number of records returned.
        // Example usage: count(MySQL_Recordset);
        return $this->num_rows;
        }
        
        
    function current() {
        // Part of the Iterator interface.  Used for getting the value of the current record.
        return $this->fetchRow();
        }
        
        
    function key() {
        // Part of the Iterator interface.  Used for getting current number (array index) of the current record.
        return $this->pointer;
        }
    
    
    function next() {
        // Part of the Iterator interface.  Used for setting up record info properties pointing to the next record in line.
        $this->pointer++;
        }
        
        
    function rewind() {
        // Part of the Iterator interface.  Used for setting up record info properties pointing to the very first record in line.
        $this->pointer = 0;
        }
        
        
    function valid() {
        // Part of the Iterator interface.  Used to determine if the current record in line exists, or if end of query has been reached.
        return $this->offsetExists($this->pointer);
        }
        
        
    function offsetExists($offset) {
        // Part of the ArrayAccess interface.  Used to determine if the selected record in the set actually exists or is out of bounds.
        // Example: 'isset(MySQL_Recordset[22])' returns true if there were 23 or more records returned from the query.
        return ((int) $offset < $this->num_rows AND (int) $offset >= 0);
        }
        
        
    function offsetGet($offset) {
        // Part of the ArrayAccess interface.  Used to retrieve the assoc array of the chosen record in line.
        // Example: 'print_r(MySQL_Recordset[22])' prints the 23rd record returned by the query.
        if (!$this->offsetExists($offset))
            throw new OutOfBoundsException('Out of range of result set.');
        // allow index referencing without altering "internal array pointer"
        return $this->fetchRow($offset);
        }
        
        
    function offsetSet($offset, $value) {
        // Part of the ArrayAccess interface.  Used to set the numbered record.
        // Example: 'MySQL_Recordset[22] = array(1, "some title", $date)' to add/update a record in the table referenced in single table selects.
        // Not currently implemented in this code, especially since multiple tables may be returned or no keys to work with
        throw new BadMethodCallException('Add/update method not yet supported.');
        }

        
    function offsetUnset($offset) {
        // Part of the ArrayAccess interface.  Used to delete the numbered record referenced in single table selects.
        // Example: 'unset(MySQL_Recordset[22])' to delete that record from the table.
        // Not currently implemented in this code, especially since multiple tables may be returned or no keys to work with
        throw new BadMethodCallException('Delete method not yet supported.');
        }
        
    }